- 问题描述:并发20个线程,每个线程使用一个单独的BosClient去执行5G文件的PUT/GET操作,并比较下载文件和原文件的MD5,发现偶尔出现文件MD5对不上,文件大小大于5G的情况的情况,出现几率大概1%;
- 问题定位:
- 查看sdk日志和BOS日志,发现该情况下client发送了2次GET请求,第一次返回200但是err msg是PartialContentError
- 进一步查看nginx error log发现是客户端主动关闭了连接,可能是因为TCP连接超时等原因导致client主动关闭连接
- 但是从client日只看并没有触发重试机制,那么多出来的GET请求可能就是sdk用到的http库主动发起的
- 问题原因:
- 定位发现sdk引用了第三方库rest-client,rest-client又引用了ruby语言自带的net::http库来发起http请求
- 查看net::http库源码发现,默认自带了一次重试机制;重试时不会对重置读取到的body stream,而是会继续追加写,导致文件大小大于5G
- Net::HTTP read_timeout causes double requests
解决方案:
net::http库默认重试一次的机制不合理,因此有用户提出了bug,建议retry次数可配置Net::HTTP retries idempotent requests once after a timeout, but its not configurable;AWS ruby sdk也发现了这一问题,提交了PR,不过要到ruby 2.5版本才支持直接设置重试次数。考虑到版本向下兼容问题,我们采取monkey patching的方式来解决这一问题,两个思路:方案一:第二次重试的时候将body stream重置到起始位置
module Net class HTTP def transport_request(req) count = 0 begin begin_transport req res = catch(:response) { req.exec @socket, @curr_http_version, edit_path(req.path) begin res = HTTPResponse.read_new(@socket) res.decode_content = req.decode_content end while res.kind_of?(HTTPContinue) res.uri = req.uri res } res.reading_body(@socket, req.response_body_permitted?) { yield res if block_given? } rescue Net::OpenTimeout raise rescue Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, # avoid a dependency on OpenSSL defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError, Timeout::Error => exception if count == 0 && IDEMPOTENT_METHODS_.include?(req.method) count += 1 @socket.close if @socket and not @socket.closed? D "Conn close because of error #{exception}, and retry" // 添加重置body_stream操作 if req.body_stream if req.body_stream.respond_to?(:rewind) req.body_stream.rewind else raise end end retry end D "Conn close because of error #{exception}" @socket.close if @socket and not @socket.closed? raise end end_transport req, res res rescue => exception D "Conn close because of error #{exception}" @socket.close if @socket and not @socket.closed? raise exception end end end
- 方案二:去除重试机制,删掉retry语句
参考链接